import { $set } from '@feng3d/serialization';
import { Clock, Color, Matrix4, MeshLambertMaterial3D, Node3D, PerspectiveCamera3D, PlaneGeometry3D, Scene3D, StatsComponent, TextureLoader3D, WebGLRenderer3D } from 'feng3d-three';
import { FirstPersonControls3D } from '../../extension/controls/FirstPersonControls3D';
import { ImprovedNoise } from '../../extension/thirdparty/three/imports';
import { BufferGeometryUtils3D } from '../../extension/utils/BufferGeometryUtils3D';

const rootPath = '../../';

let container: HTMLElement;
let stats: StatsComponent;

let camera: PerspectiveCamera3D;
let controls: FirstPersonControls3D;
let scene: Scene3D;
let renderer: WebGLRenderer3D;

const worldWidth = 128;
const worldDepth = 128;
const worldHalfWidth = worldWidth / 2;
const worldHalfDepth = worldDepth / 2;
const data = generateHeight(worldWidth, worldDepth);

const clock = new Clock();

init();
animate();

function init()
{
    container = document.getElementById('container');

    camera = new Node3D().addComponent('PerspectiveCamera3D', {
        fov: 60, aspect: window.innerWidth / window.innerHeight, near: 1, far: 20000
    });
    camera.entity.position.y = getY(worldHalfWidth, worldHalfDepth) * 100 + 100;

    scene = new Node3D().addComponent('Scene3D');
    scene.background = new Color(0xbfd1e5);

    // sides

    const matrix = new Matrix4();

    const pxGeometry = $set(new PlaneGeometry3D(), { width: 100, height: 100 });
    pxGeometry.attributes.uv.array[1] = 0.5;
    pxGeometry.attributes.uv.array[3] = 0.5;
    pxGeometry.rotateY(Math.PI / 2);
    pxGeometry.translate(50, 0, 0);

    const nxGeometry = $set(new PlaneGeometry3D(), { width: 100, height: 100 });
    nxGeometry.attributes.uv.array[1] = 0.5;
    nxGeometry.attributes.uv.array[3] = 0.5;
    nxGeometry.rotateY(-Math.PI / 2);
    nxGeometry.translate(-50, 0, 0);

    const pyGeometry = $set(new PlaneGeometry3D(), { width: 100, height: 100 });
    pyGeometry.attributes.uv.array[5] = 0.5;
    pyGeometry.attributes.uv.array[7] = 0.5;
    pyGeometry.rotateX(-Math.PI / 2);
    pyGeometry.translate(0, 50, 0);

    const pzGeometry = $set(new PlaneGeometry3D(), { width: 100, height: 100 });
    pzGeometry.attributes.uv.array[1] = 0.5;
    pzGeometry.attributes.uv.array[3] = 0.5;
    pzGeometry.translate(0, 0, 50);

    const nzGeometry = $set(new PlaneGeometry3D(), { width: 100, height: 100 });
    nzGeometry.attributes.uv.array[1] = 0.5;
    nzGeometry.attributes.uv.array[3] = 0.5;
    nzGeometry.rotateY(Math.PI);
    nzGeometry.translate(0, 0, -50);

    //

    const geometries: PlaneGeometry3D[] = [];

    for (let z = 0; z < worldDepth; z++)
    {
        for (let x = 0; x < worldWidth; x++)
        {
            const h = getY(x, z);

            matrix.makeTranslation(
                x * 100 - worldHalfWidth * 100,
                h * 100,
                z * 100 - worldHalfDepth * 100
            );

            const px = getY(x + 1, z);
            const nx = getY(x - 1, z);
            const pz = getY(x, z + 1);
            const nz = getY(x, z - 1);

            geometries.push(pyGeometry.clone().applyMatrix4(matrix));

            if ((px !== h && px !== h + 1) || x === 0)
            {
                geometries.push(pxGeometry.clone().applyMatrix4(matrix));
            }

            if ((nx !== h && nx !== h + 1) || x === worldWidth - 1)
            {
                geometries.push(nxGeometry.clone().applyMatrix4(matrix));
            }

            if ((pz !== h && pz !== h + 1) || z === worldDepth - 1)
            {
                geometries.push(pzGeometry.clone().applyMatrix4(matrix));
            }

            if ((nz !== h && nz !== h + 1) || z === 0)
            {
                geometries.push(nzGeometry.clone().applyMatrix4(matrix));
            }
        }
    }

    const geometry = BufferGeometryUtils3D.mergeBufferGeometries(geometries);
    geometry.computeBoundingSphere();

    const texture = new TextureLoader3D().load(`${rootPath}textures/minecraft/atlas.png`);
    texture.magFilter = 'NearestFilter';

    const mesh = new Node3D().addComponent('Mesh3D', {
        geometry, material: $set(new MeshLambertMaterial3D(), { map: texture, side: 'DoubleSide' })
    });
    scene.entity.addChild(mesh.entity);

    const ambientLight = new Node3D().addComponent('AmbientLight3D', { color: { value: 0xcccccc } });
    scene.entity.addChild(ambientLight.entity);

    const directionalLight = new Node3D().addComponent('DirectionalLight3D', { color: { value: 0xffffff }, intensity: 2 });
    directionalLight.entity.position.set(1, 1, 0.5).normalize();
    scene.entity.addChild(directionalLight.entity);

    renderer = scene.addComponent('WebGLRenderer3D', { parameters: { antialias: true } });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    container.appendChild(renderer.domElement);

    controls = new FirstPersonControls3D(camera, renderer.domElement);

    controls.movementSpeed = 1000;
    controls.lookSpeed = 0.125;
    controls.lookVertical = true;

    stats = scene.addComponent('StatsComponent');
    container.appendChild(stats.dom);

    //

    window.addEventListener('resize', onWindowResize);
}

function onWindowResize()
{
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);

    controls.handleResize();
}

function generateHeight(width: number, height: number)
{
    const data: number[] = []; const perlin = new ImprovedNoise();
    const size = width * height;
    const z = Math.random() * 100;

    let quality = 2;

    for (let j = 0; j < 4; j++)
    {
        if (j === 0) for (let i = 0; i < size; i++) data[i] = 0;

        for (let i = 0; i < size; i++)
        {
            const x = i % width; const
                y = (i / width) | 0;
            data[i] += perlin.noise(x / quality, y / quality, z) * quality;
        }

        quality *= 4;
    }

    return data;
}

function getY(x: number, z: number)
{
    return (data[x + z * worldWidth] * 0.15) | 0;
}

//

function animate()
{
    requestAnimationFrame(animate);

    render();
    stats.update();
}

function render()
{
    controls.update(clock.getDelta());
    renderer.render(scene, camera);
}
